<?php

namespace S\WeChat\Exception;

use S\Exception as SException;
use S\WeChat\Exception\Internal as Exception;

/**
 * 微信模块相关异常
 *
 * @package Ext\Wechat\Abstraction
 */
abstract class Abstraction extends SException
{
    const _DEFAULT_ERR_CODE = '3199999';
    const _DEFAULT_ERR_MSG  = '未定义的微信模块内部错误';

    const _CODE_MAX = 3999999;
    const _CODE_MIN = 3100000;

    const _CODE_PREFIX_DAO_INTERNAL = '310';
    const _CODE_PREFIX_DAO_DB       = '311';
    const _CODE_PREFIX_DAO_HTTP_API = '312';
    const _CODE_PREFIX_DAO_CACHE    = '313';
    const _CODE_PREFIX_DAO_EXTERNAL = '38';

    const _MSG_SEARCH_PREFIX = '@';
    const _MSG_SEARCH_SUFFIX = '@';

    const _MSG_PREFIX = '[微信模块]';

    protected $code_prefix          = self::_CODE_PREFIX_DAO_INTERNAL;
    protected $code_message_mapping = [];

    protected $code_message_mapping_default = [
        self::_DEFAULT_ERR_CODE => self::_DEFAULT_ERR_MSG,
    ];

    /**
     * Exception constructor.
     *
     * @param string          $code    错误码
     * @param array           $replace 替换消息中的词
     * @param \Exception|null $previous
     */
    final public function __construct($code, $replace = [], \Exception $previous = null)
    {
        $this->setCodeMessageMapping()->checkCodeRange($code);
        parent::__construct(
            $this->getErrorMessage($code, $replace),
            "{$this->code_prefix}{$code}",
            $previous
        );
    }

    /**
     * 抛出异常
     *
     * 抛异常之前会调用$this->beforeThrow()方法.
     *
     * @return void
     *
     * @throws \S\WeChat\Exception\Abstraction
     */
    public function throwMe()
    {
        if ($this->beforeThrow()) {
            throw $this;
        }
        (new Exception(Exception::CODE_BEFORE_THROW_ERROR))->throwMe();
    }

    /**
     * 获取错误消息
     *
     * @param string $code     错误码
     * @param array  $replacer 替换消息中的词
     * @param string $default  默认错误码:在$code未定义时抛出
     *
     * @return string 消息
     * @throws \S\WeChat\Exception\Internal
     */
    protected function getErrorMessage($code, $replacer = [], $default = self::_DEFAULT_ERR_CODE)
    {
        if (($code = strval($code)) && array_key_exists($code, $this->code_message_mapping)) {
            $message = $this->code_message_mapping[$code];
            if ($replace_search = $this->isMessageReplacing($message)) {
                if (empty($replacer)) {
                    $replace = ['@replace@' => implode($replace_search, ',')];
                    (new Exception(Exception::CODE_REQUIRE_REPLACING, $replace))->throwMe();
                }
                if (1 == count($replace_search) && !is_array($replacer)) {
                    $replacer = [$replace_search[0] => $replacer];
                }
                foreach ($replacer as $search => $replace) {
                    $message = str_replace($search, $replace, $message);
                }
                return static::_MSG_PREFIX . $message;
            }
        };
        return $this->getErrorMessage($default);
    }

    /**
     * 设置错误码和消息的映射
     *
     * @return $this
     */
    protected function setCodeMessageMapping()
    {
        $this->code_message_mapping = array_merge($this->getCodeMessages(), $this->code_message_mapping_default);
        return $this;
    }

    /**
     * 设置错误码前缀
     *
     * 如果错误码前缀为'311',抛异常前传入的错误码为'0001',则最终抛出的错误码为'3110001'.
     *
     * @return $this
     */
    protected function setCodePrefix()
    {
        $this->code_prefix = $this->getCodePrefix() ?: '';
        return $this;
    }

    /**
     * 检查错误码范围
     *
     * @param string $code 错误码:不含前缀
     *
     * @return $this
     * @throws \S\WeChat\Exception\Internal
     */
    protected function checkCodeRange($code)
    {
        $code = intval(static::getCodePrefix() . $code);

        //外部异常时直接用外部服务返回的错误码
        if ($this instanceof \S\WeChat\Exception\External) {
            return $this;
        }
        if (self::_CODE_MAX < $code || self::_CODE_MIN > $code) {
            (new Exception(Exception::CODE_CODE_OVER_RANGE, $code))->throwMe();
        }
        return $this;
    }

    /**
     * 获取匹配错误消息中需要替换的关键词的正则表达式
     *
     * @return string
     */
    protected function getReplacePattern()
    {
        return '(' . self::_MSG_SEARCH_PREFIX . '[\d\w\W_]+?' . self::_MSG_SEARCH_SUFFIX . ')';
    }

    /**
     * 检查一个错误消息中是否包含需要被替换的内容
     *
     * @param string $message         错误消息
     * @param string $replace_pattern 查找规则:一个正则表达式
     *
     * @return array 错误消息中所有需要被替换的关键词:[@keyword_0@, @keyword_1@, ...]
     */
    protected function isMessageReplacing($message, $replace_pattern = '')
    {
        if (preg_match_all($replace_pattern ?: $this->getReplacePattern(), $message, $replace_search)) {
            return $replace_search[0];
        }
        return [];
    }

    /**
     * 获取错误码前缀
     *
     * @return mixed
     */
    abstract protected function getCodePrefix();

    /**
     * 获取所有错误码和错误消息的映射
     *
     * @return mixed
     */
    abstract protected function getCodeMessages();

    /**
     * 异常抛出之前的回调
     *
     * @return bool
     */
    protected function beforeThrow()
    {
        return true;
    }


}